#include "rive/text/text.hpp"
using namespace rive;
#ifdef WITH_RIVE_TEXT
#include "rive/text_engine.hpp"
#include "rive/component_dirt.hpp"
#include "rive/text/utf.hpp"
#include "rive/text/text_style.hpp"
#include "rive/text/text_value_run.hpp"
#include "rive/text/text_modifier_group.hpp"
#include "rive/shapes/paint/shape_paint.hpp"
#include "rive/artboard.hpp"
#include "rive/factory.hpp"
#include "rive/clip_result.hpp"
#include <limits>

void GlyphItr::tryAdvanceRun()
{
    while (true)
    {
        auto run = *m_run;
        if (m_glyphIndex == m_line->endGlyphIndex(run) &&
            run != m_line->lastRun())
        {
            m_run++;
            m_glyphIndex = m_line->startGlyphIndex(*m_run);
        }
        else
        {
            break;
        }
    }
}
GlyphItr& GlyphItr::operator++()
{
    auto run = *m_run;
    m_glyphIndex += run->dir() == TextDirection::ltr ? 1 : -1;
    tryAdvanceRun();
    return *this;
}

static void reverseRuns(const GlyphRun** runs, int count)
{
    int halfCount = count / 2;
    int finalIndex = count - 1;

    for (int index = 0; index < halfCount; index++)
    {
        int tieIndex = finalIndex - index;

        const GlyphRun* tempRun = runs[index];
        runs[index] = runs[tieIndex];
        runs[tieIndex] = tempRun;
    }
}

OrderedLine::OrderedLine(const Paragraph& paragraph,
                         const GlyphLine& line,
                         float lineWidth,
                         bool wantEllipsis,
                         bool isEllipsisLineLast,
                         GlyphRun* ellipsisRun) :
    m_startGlyphIndex(line.startGlyphIndex), m_endGlyphIndex(line.endGlyphIndex)
{
    std::vector<const GlyphRun*> logicalRuns;
    const SimpleArray<GlyphRun>& glyphRuns = paragraph.runs;

    if (!wantEllipsis || !buildEllipsisRuns(logicalRuns,
                                            paragraph,
                                            line,
                                            lineWidth,
                                            isEllipsisLineLast,
                                            ellipsisRun))
    {
        for (uint32_t i = line.startRunIndex; i < line.endRunIndex + 1; i++)
        {
            logicalRuns.push_back(&glyphRuns[i]);
        }

        if (!logicalRuns.empty())
        {
            m_startLogical = logicalRuns.front();
            m_endLogical = logicalRuns.back();
        }
    }

    uint8_t maxLevel = 0;
    for (auto run : logicalRuns)
    {
        if (run->level > maxLevel)
        {
            maxLevel = run->level;
        }
    }
    for (uint8_t newLevel = maxLevel; newLevel > 0; newLevel--)
    {
        for (int start = (int)(logicalRuns.size()) - 1; start >= 0; start--)
        {
            if (logicalRuns[start]->level >= newLevel)
            {
                int count = 1;
                for (; start > 0 && logicalRuns[start - 1]->level >= newLevel;
                     start--)
                {
                    count++;
                }
                reverseRuns(logicalRuns.data() + start, count);
            }
        }
    }

    m_runs = std::move(logicalRuns);
}

static void appendUnicode(std::vector<rive::Unichar>& unichars,
                          const char text[])
{
    const uint8_t* ptr = (const uint8_t*)text;
    while (*ptr)
    {
        unichars.push_back(rive::UTF::NextUTF8(&ptr));
    }
}

bool OrderedLine::buildEllipsisRuns(std::vector<const GlyphRun*>& logicalRuns,
                                    const Paragraph& paragraph,
                                    const GlyphLine& line,
                                    float lineWidth,
                                    bool isEllipsisLineLast,
                                    GlyphRun* storedEllipsisRun)
{
    float x = 0.0f;
    const SimpleArray<GlyphRun>& glyphRuns = paragraph.runs;
    uint32_t startGIndex = line.startGlyphIndex;
    // If it's the last line we can actually early out if the whole things fits,
    // so check that first with no extra shaping.
    if (isEllipsisLineLast)
    {
        bool fits = true;

        for (uint32_t i = line.startRunIndex; i < line.endRunIndex + 1; i++)
        {
            const GlyphRun& run = glyphRuns[i];
            uint32_t endGIndex = i == line.endRunIndex
                                     ? line.endGlyphIndex
                                     : (uint32_t)run.glyphs.size();

            for (uint32_t j = startGIndex; j != endGIndex; j++)
            {
                x += run.advances[j];
                if (x > lineWidth)
                {
                    fits = false;
                    goto measured;
                }
            }
            startGIndex = 0;
        }
    measured:
        if (fits)
        {
            // It fits, just get the regular glyphs.
            return false;
        }
    }

    std::vector<Unichar> ellipsisCodePoints;
    appendUnicode(ellipsisCodePoints, "...");

    rcp<Font> ellipsisFont = nullptr;
    float ellipsisFontSize = 0.0f;

    GlyphRun ellipsisRun = {};
    float ellipsisWidth = 0.0f;

    bool ellipsisOverflowed = false;
    startGIndex = line.startGlyphIndex;

    for (uint32_t i = line.startRunIndex; i < line.endRunIndex + 1; i++)
    {
        const GlyphRun& run = glyphRuns[i];
        if (run.font != ellipsisFont && run.size != ellipsisFontSize)
        {
            // Track the latest we've checked (even if we discard it so we don't
            // try to do this again for this ellipsis).
            ellipsisFont = run.font;
            ellipsisFontSize = run.size;

            // Get the next shape so we can check if it fits, otherwise keep
            // using the last one.
            TextRun ellipsisRuns[] = {{ellipsisFont,
                                       ellipsisFontSize,
                                       run.lineHeight,
                                       run.letterSpacing,
                                       (uint32_t)ellipsisCodePoints.size()}};
            auto nextEllipsisShape =
                ellipsisFont->shapeText(ellipsisCodePoints,
                                        Span<TextRun>(ellipsisRuns, 1));

            // Hard assumption one run and para
            const Paragraph& para = nextEllipsisShape[0];
            const GlyphRun& nextEllipsisRun = para.runs.front();

            float nextEllipsisWidth = 0;
            for (size_t j = 0; j < nextEllipsisRun.glyphs.size(); j++)
            {
                nextEllipsisWidth += nextEllipsisRun.advances[j];
            }

            if (ellipsisRun.font == nullptr ||
                x + nextEllipsisWidth <= lineWidth)
            {
                // This ellipsis still fits, go ahead and use it. Otherwise
                // stick with the old one.
                ellipsisWidth = nextEllipsisWidth;
                ellipsisRun = std::move(para.runs.front());
            }
        }

        uint32_t endGIndex = i == line.endRunIndex
                                 ? line.endGlyphIndex
                                 : (uint32_t)run.glyphs.size();
        for (uint32_t j = startGIndex; j != endGIndex; j++)
        {
            float advance = run.advances[j];
            if (x + advance + ellipsisWidth > lineWidth)
            {
                m_endGlyphIndex = j;
                ellipsisOverflowed = true;
                break;
            }
            x += advance;
        }
        startGIndex = 0;
        logicalRuns.push_back(&run);
        m_endLogical = &run;

        if (ellipsisOverflowed && ellipsisRun.font != nullptr)
        {
            *storedEllipsisRun = std::move(ellipsisRun);
            logicalRuns.push_back(storedEllipsisRun);
            break;
        }
    }

    // There was enough space for it, so let's add the ellipsis (if we didn't
    // already). Note that we already checked if this is the last line and found
    // that the whole text didn't fit.
    if (!ellipsisOverflowed && ellipsisRun.font != nullptr)
    {
        *storedEllipsisRun = std::move(ellipsisRun);
        logicalRuns.push_back(storedEllipsisRun);
    }
    m_startLogical = storedEllipsisRun == logicalRuns.front()
                         ? nullptr
                         : logicalRuns.front();
    return true;
}

Vec2D Text::measureLayout(float width,
                          LayoutMeasureMode widthMode,
                          float height,
                          LayoutMeasureMode heightMode)
{
    return measure(Vec2D(widthMode == LayoutMeasureMode::undefined
                             ? std::numeric_limits<float>::max()
                             : width,
                         heightMode == LayoutMeasureMode::undefined
                             ? std::numeric_limits<float>::max()
                             : height));
}

void Text::controlSize(Vec2D size,
                       LayoutScaleType widthScaleType,
                       LayoutScaleType heightScaleType)
{
    if (m_layoutWidth != size.x || m_layoutHeight != size.y ||
        m_layoutWidthScaleType != (uint8_t)widthScaleType ||
        m_layoutHeightScaleType != (uint8_t)heightScaleType)
    {
        m_layoutWidth = size.x;
        m_layoutHeight = size.y;
        m_layoutWidthScaleType = (uint8_t)widthScaleType;
        m_layoutHeightScaleType = (uint8_t)heightScaleType;
        markShapeDirty(false);
    }
}

TextSizing Text::effectiveSizing() const
{
    if (m_layoutWidthScaleType == std::numeric_limits<uint8_t>::max() ||
        m_layoutWidthScaleType == (uint8_t)LayoutScaleType::hug ||
        m_layoutHeightScaleType == (uint8_t)LayoutScaleType::hug)
    {
        return sizing();
    }
    return TextSizing::fixed;
}

void Text::buildRenderStyles()
{
    for (TextStyle* style : m_renderStyles)
    {
        style->rewindPath();
    }
    m_renderStyles.clear();
    if (m_shape.empty())
    {
        m_bounds = AABB(0.0f, 0.0f, 0.0f, 0.0f);
        return;
    }
    const float paragraphSpace = paragraphSpacing();

    // Build up ordered runs as we go.
    int paragraphIndex = 0;
    float y = 0.0f;
    float minY = 0.0f;
    float maxWidth = 0.0f;
    if (textOrigin() == TextOrigin::baseline && !m_lines.empty() &&
        !m_lines[0].empty())
    {
        y -= m_lines[0][0].baseline;
        minY = y;
    }

    int ellipsisLine = -1;
    bool isEllipsisLineLast = false;
    // Find the line to put the ellipsis on (line before the one that
    // overflows).
    bool wantEllipsis = overflow() == TextOverflow::ellipsis &&
                        effectiveSizing() == TextSizing::fixed &&
                        verticalAlign() == VerticalTextAlign::top;

    int lastLineIndex = -1;
    for (const SimpleArray<GlyphLine>& paragraphLines : m_lines)
    {
        const Paragraph& paragraph = m_shape[paragraphIndex++];
        for (const GlyphLine& line : paragraphLines)
        {
            const GlyphRun& endRun = paragraph.runs[line.endRunIndex];
            const GlyphRun& startRun = paragraph.runs[line.startRunIndex];
            float width = endRun.xpos[line.endGlyphIndex] -
                          startRun.xpos[line.startGlyphIndex] -
                          endRun.letterSpacing;
            if (width > maxWidth)
            {
                maxWidth = width;
            }
            lastLineIndex++;
            if (wantEllipsis && y + line.bottom <= effectiveHeight())
            {
                ellipsisLine++;
            }
        }

        if (!paragraphLines.empty())
        {
            y += paragraphLines.back().bottom;
        }
        y += paragraphSpace;
    }
    auto totalHeight = y;
    if (wantEllipsis && ellipsisLine == -1)
    {
        // Nothing fits, just show the first line and ellipse it.
        ellipsisLine = 0;
    }
    isEllipsisLineLast = lastLineIndex == ellipsisLine;

    int lineIndex = 0;
    paragraphIndex = 0;
    switch (effectiveSizing())
    {
        case TextSizing::autoWidth:
            m_bounds =
                AABB(0.0f, minY, maxWidth, std::max(minY, y - paragraphSpace));
            break;
        case TextSizing::autoHeight:
            m_bounds = AABB(0.0f,
                            minY,
                            effectiveWidth(),
                            std::max(minY, y - paragraphSpace));
            break;
        case TextSizing::fixed:
            m_bounds =
                AABB(0.0f, minY, effectiveWidth(), minY + effectiveHeight());
            break;
    }

    auto verticalAlignOffset = 0.0f;
    switch (verticalAlign())
    {
        case VerticalTextAlign::middle:
            verticalAlignOffset = (totalHeight - m_bounds.height()) / 2;
            break;
        case VerticalTextAlign::bottom:
            verticalAlignOffset = totalHeight - m_bounds.height();
            break;
        default:
            break;
    }

    // Build the clip path if we want it.
    if (overflow() == TextOverflow::clipped)
    {
        if (m_clipRenderPath == nullptr)
        {
            m_clipRenderPath = artboard()->factory()->makeEmptyRenderPath();
        }
        else
        {
            m_clipRenderPath->rewind();
        }

        AABB bounds = localBounds();

        m_clipRenderPath->addRect(bounds.minX + bounds.width() * originX(),
                                  bounds.minY + bounds.height() * originY() +
                                      verticalAlignOffset,
                                  bounds.width(),
                                  bounds.height());
    }
    else
    {
        m_clipRenderPath = nullptr;
    }

    y = 0;
    if (textOrigin() == TextOrigin::baseline && !m_lines.empty() &&
        !m_lines[0].empty())
    {
        y -= m_lines[0][0].baseline;
    }
    paragraphIndex = 0;

    bool hasModifiers = haveModifiers();
    if (hasModifiers)
    {
        uint32_t textSize = (uint32_t)m_styledText.unichars().size();
        for (TextModifierGroup* modifierGroup : m_modifierGroups)
        {
            modifierGroup->computeCoverage(textSize);
        }
    }
    float minX = std::numeric_limits<float>::max();
    bool drawLine = true;
    for (const SimpleArray<GlyphLine>& paragraphLines : m_lines)
    {
        const Paragraph& paragraph = m_shape[paragraphIndex++];
        for (const GlyphLine& line : paragraphLines)
        {
            drawLine = true;
            switch (overflow())
            {
                case TextOverflow::hidden:
                    if (effectiveSizing() == TextSizing::fixed)
                    {
                        switch (verticalAlign())
                        {
                            case VerticalTextAlign::top:
                                if (y + line.bottom > effectiveHeight())
                                {
                                    goto skipLines;
                                }
                                break;
                            case VerticalTextAlign::middle:
                                if (y + line.top <
                                    totalHeight / 2 - effectiveHeight() / 2)
                                {
                                    drawLine = false;
                                }
                                if (y + line.bottom >
                                    totalHeight / 2 + effectiveHeight() / 2)
                                {
                                    drawLine = false;
                                    goto skipLines;
                                }
                                break;
                            case VerticalTextAlign::bottom:
                                if (y + line.top <
                                    totalHeight - effectiveHeight())
                                {
                                    continue;
                                }
                                break;
                        }
                    }
                    break;
                case TextOverflow::clipped:
                    if (effectiveSizing() == TextSizing::fixed)
                    {
                        switch (verticalAlign())
                        {
                            case VerticalTextAlign::top:
                                if (y + line.top > effectiveHeight())
                                {
                                    goto skipLines;
                                }
                                break;
                            case VerticalTextAlign::middle:
                                if (y + line.bottom <
                                    totalHeight / 2 - effectiveHeight() / 2)
                                {
                                    drawLine = false;
                                }
                                if (y + line.top >
                                    totalHeight / 2 + effectiveHeight() / 2)
                                {
                                    goto skipLines;
                                }
                                break;
                            case VerticalTextAlign::bottom:
                                if (y + line.bottom <
                                    totalHeight - effectiveHeight())
                                {
                                    drawLine = false;
                                }
                                break;
                        }
                    }
                    break;
                default:
                    break;
            }

            if (lineIndex >= m_orderedLines.size())
            {
                // We need to still compute this line's ordered runs.
                m_orderedLines.emplace_back(
                    OrderedLine(paragraph,
                                line,
                                effectiveWidth(),
                                ellipsisLine == lineIndex,
                                isEllipsisLineLast,
                                &m_ellipsisRun));
            }

            const OrderedLine& orderedLine = m_orderedLines[lineIndex];
            float x = line.startX;
            minX = std::min(x, minX);
            float renderY = y + line.baseline;
            for (auto glyphItr : orderedLine)
            {
                const GlyphRun* run = std::get<0>(glyphItr);
                size_t glyphIndex = std::get<1>(glyphItr);

                const Font* font = run->font.get();
                const Vec2D& offset = run->offsets[glyphIndex];

                GlyphID glyphId = run->glyphs[glyphIndex];
                float advance = run->advances[glyphIndex];

                RawPath path = font->getPath(glyphId);

                uint32_t textIndex = 0;
                uint32_t glyphCount = 0;
                if (hasModifiers)
                {
                    textIndex = run->textIndices[glyphIndex];
                    glyphCount = m_glyphLookup.count(textIndex);

                    float centerX = advance / 2.0f;
                    Mat2D transform = Mat2D::fromScaleAndTranslation(run->size,
                                                                     run->size,
                                                                     -centerX,
                                                                     0.0f);
                    for (TextModifierGroup* modifierGroup : m_modifierGroups)
                    {
                        float coverage =
                            modifierGroup->glyphCoverage(textIndex, glyphCount);
                        modifierGroup->transform(coverage, transform);
                    }
                    transform =
                        Mat2D::fromTranslate(centerX + x + offset.x,
                                             y + line.baseline + offset.y) *
                        transform;

                    path.transformInPlace(transform);
                }
                else
                {
                    path.transformInPlace(Mat2D(run->size,
                                                0.0f,
                                                0.0f,
                                                run->size,
                                                x + offset.x,
                                                renderY + offset.y));
                }

                x += advance;

                assert(run->styleId < m_runs.size());
                TextStyle* style = m_runs[run->styleId]->style();
                // TextValueRun::onAddedDirty botches loading if it cannot
                // resolve a style, so we're confident we have a style here.
                assert(style != nullptr);
                // Consider this the "local" opacity.
                float opacity = 1.0f;
                if (hasModifiers)
                {
                    for (TextModifierGroup* modifierGroup : m_modifierGroups)
                    {
                        if (modifierGroup->modifiesOpacity())
                        {
                            float coverage =
                                modifierGroup->glyphCoverage(textIndex,
                                                             glyphCount);
                            opacity = modifierGroup->computeOpacity(opacity,
                                                                    coverage);
                        }
                    }
                }
                if (drawLine)
                {

                    if (style->addPath(path, opacity))
                    {
                        // This was the first path added to the style, so let's
                        // mark it in our draw list.
                        m_renderStyles.push_back(style);
                        style->propagateOpacity(renderOpacity());
                    }
                }
            }
            if (lineIndex == ellipsisLine)
            {
                goto skipLines;
            }
            lineIndex++;
        }
        if (!paragraphLines.empty())
        {
            y += paragraphLines.back().bottom;
        }
        y += paragraphSpace;
    }
skipLines:
    auto scale = 1.0f;
    auto xOffset = -m_bounds.width() * originX();
    auto yOffset = -m_bounds.height() * originY();
    if (overflow() == TextOverflow::fit)
    {
        auto xScale = (effectiveSizing() != TextSizing::autoWidth &&
                       maxWidth > m_bounds.width())
                          ? m_bounds.width() / maxWidth
                          : 1;
        auto baseline = fitFromBaseline() ? m_lines[0][0].baseline : 0;
        auto yScale =
            (effectiveSizing() == TextSizing::fixed &&
             totalHeight > m_bounds.height())
                ? (m_bounds.height() - baseline) / (totalHeight - baseline)
                : 1;
        if (xScale != 1 || yScale != 1)
        {
            scale = std::max(0.0f, xScale > yScale ? yScale : xScale);
            yOffset += baseline * (1 - scale);
            switch ((TextAlign)alignValue())
            {
                case TextAlign::center:
                    xOffset += (m_bounds.width() - maxWidth * scale) / 2 -
                               minX * scale;
                    break;
                case TextAlign::right:
                    xOffset +=
                        m_bounds.width() - maxWidth * scale - minX * scale;
                    break;
                default:
                    break;
            }
        }
    }
    if (verticalAlign() != VerticalTextAlign::top)
    {
        if (effectiveSizing() == TextSizing::fixed)
        {
            yOffset = -m_bounds.height() * originY();
            if (verticalAlign() == VerticalTextAlign::middle)
            {
                yOffset += (m_bounds.height() - totalHeight * scale) / 2;
            }
            else if (verticalAlign() == VerticalTextAlign::bottom)
            {
                yOffset += m_bounds.height() - totalHeight * scale;
            }
        }
    }
    m_transform =
        Mat2D::fromScaleAndTranslation(scale, scale, xOffset, yOffset);
#ifdef WITH_RIVE_LAYOUT
    markLayoutNodeDirty();
#endif
}

const TextStyle* Text::styleFromShaperId(uint16_t id) const
{
    assert(id < m_runs.size());
    return m_runs[id]->style();
}

void Text::draw(Renderer* renderer)
{
    ClipResult clipResult = applyClip(renderer);
    if (clipResult == ClipResult::noClip)
    {
        // We didn't clip, so make sure to save as we'll be doing some
        // transformations.
        renderer->save();
    }
    if (clipResult != ClipResult::emptyClip)
    {
        renderer->transform(m_WorldTransform * m_transform);
        if (overflow() == TextOverflow::clipped && m_clipRenderPath)
        {
            renderer->clipPath(m_clipRenderPath.get());
        }
        for (auto style : m_renderStyles)
        {
            style->draw(renderer);
        }
    }
    renderer->restore();
}

void Text::addRun(TextValueRun* run) { m_runs.push_back(run); }

void Text::addModifierGroup(TextModifierGroup* group)
{
    m_modifierGroups.push_back(group);
}

void Text::markShapeDirty(bool sendToLayout)
{
    addDirt(ComponentDirt::Path);
    for (TextModifierGroup* group : m_modifierGroups)
    {
        group->clearRangeMaps();
    }
    markWorldTransformDirty();
#ifdef WITH_RIVE_LAYOUT
    if (sendToLayout)
    {
        markLayoutNodeDirty();
    }
#endif
}

#ifdef WITH_RIVE_LAYOUT
void Text::markLayoutNodeDirty()
{
    for (ContainerComponent* p = parent(); p != nullptr; p = p->parent())
    {
        if (p->is<LayoutComponent>())
        {
            p->as<LayoutComponent>()->markLayoutNodeDirty();
        }
    }
}
#endif

void Text::modifierShapeDirty() { addDirt(ComponentDirt::Path); }

void Text::markPaintDirty() { addDirt(ComponentDirt::Paint); }

void Text::alignValueChanged() { markShapeDirty(); }

void Text::sizingValueChanged() { markShapeDirty(); }

void Text::overflowValueChanged()
{
    if (effectiveSizing() != TextSizing::autoWidth)
    {
        markShapeDirty();
    }
}

void Text::widthChanged()
{
    if (effectiveSizing() != TextSizing::autoWidth)
    {
        markShapeDirty();
    }
}

void Text::paragraphSpacingChanged() { markPaintDirty(); }

void Text::heightChanged()
{
    if (effectiveSizing() == TextSizing::fixed)
    {
        markShapeDirty();
    }
}

void StyledText::clear()
{
    m_value.clear();
    m_runs.clear();
}

bool StyledText::empty() const { return m_runs.empty(); }

void StyledText::append(rcp<Font> font,
                        float size,
                        float lineHeight,
                        float letterSpacing,
                        const std::string& text,
                        uint16_t styleId)
{
    const uint8_t* ptr = (const uint8_t*)text.c_str();
    uint32_t n = 0;
    while (*ptr)
    {
        m_value.push_back(UTF::NextUTF8(&ptr));
        n += 1;
    }
    m_runs.push_back(
        {std::move(font), size, lineHeight, letterSpacing, n, 0, styleId});
}

bool Text::makeStyled(StyledText& styledText, bool withModifiers) const
{
    styledText.clear();
    uint16_t runIndex = 0;
    for (auto valueRun : m_runs)
    {
        auto style = valueRun->style();
        const std::string& text = valueRun->text();
        if (style == nullptr || style->font() == nullptr || text.empty())
        {
            runIndex++;
            continue;
        }
        styledText.append(style->font(),
                          style->fontSize(),
                          style->lineHeight(),
                          style->letterSpacing(),
                          text,
                          runIndex++);
    }
    if (withModifiers)
    {
        for (TextModifierGroup* group : m_modifierGroups)
        {
            group->applyShapeModifiers(*this, styledText);
        }
    }
    return !styledText.empty();
}

SimpleArray<SimpleArray<GlyphLine>> Text::BreakLines(
    const SimpleArray<Paragraph>& paragraphs,
    float width,
    TextAlign align,
    TextWrap wrap)
{
    bool autoWidth = width == -1.0f;
    float paragraphWidth = width;

    SimpleArray<SimpleArray<GlyphLine>> lines(paragraphs.size());

    size_t paragraphIndex = 0;
    for (auto& para : paragraphs)
    {
        lines[paragraphIndex] = GlyphLine::BreakLines(
            para.runs,
            (autoWidth || wrap == TextWrap::noWrap) ? -1.0f : width);
        if (autoWidth)
        {
            paragraphWidth = std::max(
                paragraphWidth,
                GlyphLine::ComputeMaxWidth(lines[paragraphIndex], para.runs));
        }
        paragraphIndex++;
    }
    paragraphIndex = 0;
    for (auto& para : paragraphs)
    {
        GlyphLine::ComputeLineSpacing(paragraphIndex == 0,
                                      lines[paragraphIndex],
                                      para.runs,
                                      paragraphWidth,
                                      align);
        paragraphIndex++;
    }
    return lines;
}

bool Text::modifierRangesNeedShape() const
{
    for (const TextModifierGroup* modifierGroup : m_modifierGroups)
    {
        if (modifierGroup->needsShape())
        {
            return true;
        }
    }
    return false;
}

void Text::update(ComponentDirt value)
{
    Super::update(value);

    if (hasDirt(value, ComponentDirt::Path))
    {
        // We have modifiers that need shaping we'll need to compute the
        // coverage right before we build the actual shape.
        bool precomputeModifierCoverage = modifierRangesNeedShape();
        bool parentIsLayoutNotArtboard =
            parent()->is<LayoutComponent>() && !parent()->is<Artboard>();
        if (precomputeModifierCoverage)
        {
            makeStyled(m_modifierStyledText, false);
            auto runs = m_modifierStyledText.runs();
            m_modifierShape =
                runs[0].font->shapeText(m_modifierStyledText.unichars(), runs);
            m_modifierLines =
                BreakLines(m_modifierShape,
                           (effectiveSizing() == TextSizing::autoWidth &&
                            !parentIsLayoutNotArtboard)
                               ? -1.0f
                               : effectiveWidth(),
                           (TextAlign)alignValue(),
                           wrap());
            m_glyphLookup.compute(m_modifierStyledText.unichars(),
                                  m_modifierShape);
            uint32_t textSize =
                (uint32_t)m_modifierStyledText.unichars().size();
            for (TextModifierGroup* group : m_modifierGroups)
            {
                group->computeRangeMap(m_modifierStyledText.unichars(),
                                       m_modifierShape,
                                       m_modifierLines,
                                       m_glyphLookup);
                group->computeCoverage(textSize);
            }
        }
        if (makeStyled(m_styledText))
        {
            auto runs = m_styledText.runs();
            m_shape = runs[0].font->shapeText(m_styledText.unichars(), runs);

            m_lines = BreakLines(m_shape,
                                 (effectiveSizing() == TextSizing::autoWidth &&
                                  !parentIsLayoutNotArtboard)
                                     ? -1.0f
                                     : effectiveWidth(),
                                 (TextAlign)alignValue(),
                                 wrap());
            if (!precomputeModifierCoverage && haveModifiers())
            {
                m_glyphLookup.compute(m_styledText.unichars(), m_shape);
                uint32_t textSize = (uint32_t)m_styledText.unichars().size();
                for (TextModifierGroup* group : m_modifierGroups)
                {
                    group->computeRangeMap(m_styledText.unichars(),
                                           m_shape,
                                           m_lines,
                                           m_glyphLookup);
                    group->computeCoverage(textSize);
                }
            }
        }
        else
        {
            m_shape = SimpleArray<Paragraph>();
            m_lines = SimpleArray<SimpleArray<GlyphLine>>();
            m_glyphLookup.clear();
        }
        m_orderedLines.clear();
        m_ellipsisRun = {};

        // Immediately build render styles so dimensions get computed.
        buildRenderStyles();
    }
    else if (hasDirt(value, ComponentDirt::Paint))
    {
        buildRenderStyles();
    }
    else if (hasDirt(value, ComponentDirt::RenderOpacity))
    {
        // Note that buildRenderStyles does this too, which is why we can get
        // away doing this in the else.
        for (TextStyle* style : m_renderStyles)
        {
            style->propagateOpacity(renderOpacity());
        }
    }
}

Vec2D Text::measure(Vec2D maxSize)
{
    if (makeStyled(m_styledText))
    {
        const float paragraphSpace = paragraphSpacing();
        auto runs = m_styledText.runs();
        auto shape = runs[0].font->shapeText(m_styledText.unichars(), runs);
        auto measuringWidth = 0.0f;
        switch (effectiveSizing())
        {
            case TextSizing::autoHeight:
            case TextSizing::fixed:
                measuringWidth = width();
                break;
            default:
                measuringWidth = std::numeric_limits<float>::max();
                break;
        }
        auto measuringWrap = maxSize.x == std::numeric_limits<float>::max()
                                 ? TextWrap::noWrap
                                 : wrap();
        auto lines = BreakLines(shape,
                                std::min(maxSize.x, measuringWidth),
                                (TextAlign)alignValue(),
                                measuringWrap);
        float y = 0;
        float computedHeight = 0.0f;
        float minY = 0;
        int paragraphIndex = 0;
        float maxWidth = 0;

        if (textOrigin() == TextOrigin::baseline && !lines.empty() &&
            !lines[0].empty())
        {
            y -= m_lines[0][0].baseline;
            minY = y;
        }
        int ellipsisLine = -1;
        bool wantEllipsis = overflow() == TextOverflow::ellipsis &&
                            sizing() == TextSizing::fixed &&
                            verticalAlign() == VerticalTextAlign::top;

        for (const SimpleArray<GlyphLine>& paragraphLines : lines)
        {
            const Paragraph& paragraph = shape[paragraphIndex++];
            for (const GlyphLine& line : paragraphLines)
            {
                const GlyphRun& endRun = paragraph.runs[line.endRunIndex];
                const GlyphRun& startRun = paragraph.runs[line.startRunIndex];
                float width = endRun.xpos[line.endGlyphIndex] -
                              startRun.xpos[line.startGlyphIndex] -
                              endRun.letterSpacing;
                if (width > maxWidth)
                {
                    maxWidth = width;
                }
                if (wantEllipsis && y + line.bottom > maxSize.y)
                {
                    if (ellipsisLine == -1)
                    {
                        // Nothing fits, just show the first line and ellipse
                        // it.
                        computedHeight = y + line.bottom;
                    }
                    goto doneMeasuring;
                }
                ellipsisLine++;
                computedHeight = y + line.bottom;
            }
            if (!paragraphLines.empty())
            {
                y += paragraphLines.back().bottom;
            }
            y += paragraphSpace;
        }
    doneMeasuring:
        Vec2D bounds;
        switch (sizing())
        {
            case TextSizing::autoWidth:
                bounds = Vec2D(maxWidth, std::max(minY, computedHeight));
                break;
            case TextSizing::autoHeight:
                bounds = Vec2D(width(), std::max(minY, computedHeight));
                break;
            case TextSizing::fixed:
                bounds = Vec2D(width(), minY + height());
                break;
        }
        return Vec2D(std::min(maxSize.x, bounds.x),
                     std::min(maxSize.y, bounds.y));
    }
    return Vec2D();
}

AABB Text::localBounds() const
{
    float width = m_bounds.width();
    float height = m_bounds.height();
    return AABB::fromLTWH(m_bounds.minX - width * originX(),
                          m_bounds.minY - height * originY(),
                          width,
                          height);
}

Core* Text::hitTest(HitInfo*, const Mat2D&)
{
    if (renderOpacity() == 0.0f)
    {
        return nullptr;
    }

    return nullptr;
}

void Text::originValueChanged()
{
    markPaintDirty();
    markWorldTransformDirty();
}

void Text::originXChanged()
{
    markPaintDirty();
    markWorldTransformDirty();
}
void Text::originYChanged()
{
    markPaintDirty();
    markWorldTransformDirty();
}

#else
// Text disabled.
void Text::draw(Renderer* renderer) {}
Core* Text::hitTest(HitInfo*, const Mat2D&) { return nullptr; }
void Text::addRun(TextValueRun* run) {}
void Text::addModifierGroup(TextModifierGroup* group) {}
void Text::markShapeDirty(bool sendToLayout) {}
void Text::update(ComponentDirt value) {}
void Text::alignValueChanged() {}
void Text::sizingValueChanged() {}
void Text::overflowValueChanged() {}
void Text::widthChanged() {}
void Text::heightChanged() {}
void Text::markPaintDirty() {}
void Text::modifierShapeDirty() {}
bool Text::modifierRangesNeedShape() const { return false; }
const TextStyle* Text::styleFromShaperId(uint16_t id) const { return nullptr; }
void Text::paragraphSpacingChanged() {}
AABB Text::localBounds() const { return AABB(); }
void Text::originValueChanged() {}
void Text::originXChanged() {}
void Text::originYChanged() {}
Vec2D Text::measureLayout(float width,
                          LayoutMeasureMode widthMode,
                          float height,
                          LayoutMeasureMode heightMode)
{
    return Vec2D();
}
void Text::controlSize(Vec2D size,
                       LayoutScaleType widthScaleType,
                       LayoutScaleType heightScaleType)
{}
#endif